{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Training\n",
    "\n",
    "This section introduces training to create your own models. You don't need to do this step \n",
    "if you use pre-trained models for {doc}`predict_cli` on your own images.\n",
    "For training, you will need a dataset. See {doc}`datasets` for instructions about a few popular datasets.\n",
    "\n",
    "Training a model can take several days even with a good GPU. Many times existing models can be refined to avoid training from scratch.\n",
    "\n",
    "The main interface for training with OpenPifPaf is the command line tool {ref}`openpifpaf.train <cli-help-train>`.\n",
    "A quick way to get started is with the training commands of the pre-trained models.\n",
    "The exact training command that was used for a model is in the first\n",
    "line of the training log file. Below are a few examples for various backbones."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Training Crop Size\n",
    "\n",
    "Training and evaluation image sizes are _unrelated_ in fully convolutional architectures like OpenPifPaf. _Instance size distribution_ between training and evaluation needs to be preserved. The size of the image that surrounds the instance to detect has little impact: for humans of height 100px, it does not matter whether that is inside a $640\\times480$ image or inside a 4k or 8k image. Therefore, you can keep the size of your training crops (selected with `--square-edge`) quite small even when evaluating on large images. The default is `--square-edge=385` for `cocokp` and that is reasonable when most of the humans are not larger than 300px during evaluation.\n",
    "![training crop size](images/trainingcrop.png)\n",
    "\n",
    "The training crop size is a major factor for GPU memory consumption and for training time."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Reproducibility\n",
    "\n",
    "Every training log file contains the command that was used to train under the key `args`. There is also a `version` key that specifies the precise version of OpenPifPaf that was used to train, like \"0.11.9+204.g987345\" (generated by [versioneer](https://github.com/python-versioneer/python-versioneer)). This means that the last tag was 0.11.9, that there are 204 additional commits beyond that tag, and that the hash of the current commit is \"987345\" (without the \"g\" as that simply stands for `git`). If you had uncommitted changes, there is an additional \"dirty\" suffix. Try to avoid \"dirty\" version numbers for training as it is impossible to tell whether you were just correcting a typo in the readme or had a substantial change that influences the training. Your local git command line client but also GitHub can compare to the short git hashes."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Multiple Datasets\n",
    "\n",
    "OpenPifPaf supports simultaneous training on multiple datasets. You specify multiple datasets by concatenating them with a `-`, e.g. to train on COCO Keypoints `cocokp` and on COCO Detections `cocodet` you specify in your training command `--dataset=cocokp-cocodet`. This also works for custom datasets defined in plugins.\n",
    "\n",
    "The samples from each dataset are tracked with weighted counters and the dataset for the next batch is chosen by the lowest counter. You specify the dataset weights (or you can think of it as an importance), for example, as `--dataset-weights 1.0 0.5`. That means that the first dataset (`cocokp` in the above example) is twice as important as the second one (e.g. `cocodet`) and two-thirds of the batches will be from the first dataset and one-third will be from the second.\n",
    "\n",
    "An epoch is over once the first of the datasets is out of samples. As the datasets are randomized during training, a different set of samples will be used from epoch to epoch. That means that eventually all data will be utilized of the larger datasets even if not all data is used during a single epoch."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Overfitting\n",
    "\n",
    "An effective way to validate the pipeline is working, loss functions are implemented correctly, encoders are correct, etc. is to overfit on a single image. You should be able to produce perfect predictions for that image. Some caveats: you need to deactivate augmentations, you must evaluate at the exact trained image size and the image should not contain crowd annotations (i.e. areas that will be ignored during training). Here is an example training command for a `cocokp` overfitting experiment:\n",
    "\n",
    "```sh\n",
    "time CUDA_VISIBLE_DEVICES=1 python3 -m openpifpaf.train \\\n",
    "  --lr=0.01 --momentum=0.9 --b-scale=5.0 \\\n",
    "  --epochs=1000 --lr-warm-up-epochs=100 \\\n",
    "  --batch-size=4 --train-batches=1 --val-batches=1 --val-interval=100 \\\n",
    "  --weight-decay=1e-5 \\\n",
    "  --dataset=cocokp --cocokp-upsample=2 --cocokp-no-augmentation \\\n",
    "  --basenet=shufflenetv2k16\n",
    "```\n",
    "\n",
    "The very first image in the training set has a large crowd annotation. Therefore, the above command overfits on four images.\n",
    "Here is the corresponding predict command for the [second image](https://www.flickr.com/photos/infiniteworld/5341741494/) in the training set:\n",
    "\n",
    "```sh\n",
    "python3 -m openpifpaf.predict \\\n",
    "  --checkpoint outputs/shufflenetv2k16-201007-153356-cocokp-1de9503b.pkl \\\n",
    "  --debug-indices cif:5 caf:5 --debug-images --long-edge=385 --loader-workers=0 \\\n",
    "  --save-all --image-output all-images/overfitted.jpeg \\\n",
    "  data-mscoco/images/train2017/000000262146.jpg\n",
    "```\n",
    "\n",
    "While the field predictions are almost perfect, it will predict two annotations while there is only one ground truth annotation. That is because the standard skeleton has no connection that goes directly from left-ear to nose and the left-eye is not visible and not annotated. You can add `--dense-connections` to include additional connections in the skeleton which will connect the nose keypoint to the rest of the skeleton."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ShuffleNet\n",
    "\n",
    "ShuffleNet models are trained without ImageNet pretraining:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```sh\n",
    "time CUDA_VISIBLE_DEVICES=0,1 python3 -m openpifpaf.train \\\n",
    "  --lr=0.0003 --momentum=0.95 --b-scale=10.0 --clip-grad-value=10 \\\n",
    "  --epochs=250 --lr-decay 220 240 --lr-decay-epochs=10 \\\n",
    "  --batch-size=32 \\\n",
    "  --weight-decay=1e-5 \\\n",
    "  --dataset=cocokp --cocokp-upsample=2 --cocokp-extended-scale --cocokp-orientation-invariant=0.1 \\\n",
    "  --basenet=shufflenetv2k16\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can refine an existing model with the `--checkpoint` option.\n",
    "We found that for stable training, the learning rate $\\lambda$,\n",
    "the `clip-grad-value` $c$ and the $\\epsilon$ from the denominator of the\n",
    "BatchNorm should obey:\n",
    "\\begin{align}\n",
    "    \\lambda \\cdot c &\\leq \\sqrt{\\epsilon}\n",
    "\\end{align}\n",
    "where the default $\\epsilon$ is $10^{-4}$ (different from the PyTorch default).\n",
    "To fit large models in your GPU memory, reduce the batch size and learning rate by the same factor."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ResNet\n",
    "\n",
    "ResNet models are initialized with weights pre-trained on ImageNet.\n",
    "That makes their training characteristics different from ShuffleNet (i.e. they look great at the beginning of training)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Debug Plots\n",
    "\n",
    "As for all commands, you need to decide whether you want to interactively show the plots in matplotlib with `--show` (I use [itermplot](https://github.com/daleroberts/itermplot)) or to save image files with `--save-all` (defaults to the `all-images/` directory).\n",
    "\n",
    "You can inspect the ground truth fields that are used for training. Add `--debug-images` to activate all types of debug plots and, for example, select to visualize field 5 of CIF and field 5 of CAF with `--debug-indices cif:5 caf:5`.\n",
    "\n",
    "You can run this on your laptop without a GPU. When debug is enabled, the training dataset is not randomized and therefore you only need the first few images in your dataset locally."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Logs\n",
    "\n",
    "For reference, check out the log files of the pretrained models. Models with their log files are shared as release \"Assets\" in the separate [openpifpaf-torchhub](https://github.com/vita-epfl/openpifpaf-torchhub) repository: click on \"releases\" and expand \"Assets\" on the latest release.\n",
    "\n",
    "To visualize log files with {ref}`openpifpaf.logs <cli-help-logs>`:\n",
    "\n",
    "```sh\n",
    "python3 -m openpifpaf.logs \\\n",
    "  outputs/resnet50block5-pif-paf-edge401-190424-122009.pkl.log \\\n",
    "  outputs/resnet101block5-pif-paf-edge401-190412-151013.pkl.log \\\n",
    "  outputs/resnet152block5-pif-paf-edge401-190412-121848.pkl.log\n",
    "```\n",
    "\n",
    "Evaluation metrics like average precision (AP) are created with the {ref}`openpifpaf.eval <cli-help-eval>` tool.\n",
    "To produce evaluation metrics every five epochs for every new checkpoint that is created:\n",
    "\n",
    "```sh\n",
    "CUDA_VISIBLE_DEVICES=0 python3 -m openpifpaf.eval \\\n",
    "  --watch --checkpoint \"outputs/shufflenetv2k??-*-cocokp*.pkl.epoch??[0,5]\" \\\n",
    "  --loader-workers=2 \\\n",
    "  --decoder=cifcaf:0 \\\n",
    "  --dataset=cocokp --force-complete-pose --seed-threshold=0.2\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Testset Submission\n",
    "\n",
    "```sh\n",
    "CUDA_VISIBLE_DEVICES=0 python3 -m openpifpaf.eval \\\n",
    "  --checkpoint shufflenetv2k30 \\\n",
    "  --loader-workers=2 \\\n",
    "  --decoder=cifcaf:0 \\\n",
    "  --dataset=cocokp --force-complete-pose --seed-threshold=0.2 \\\n",
    "  --cocokp-eval-testdev2017 --coco-no-eval-annotation-filter --write-predictions\n",
    "```\n",
    "\n",
    "This will produce the file `shufflenetv2k30.eval-cocokp.zip` which you can submit to the competition on codalab."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Detection (experimental)\n",
    "\n",
    "```sh\n",
    "time CUDA_VISIBLE_DEVICES=0,1 python3 -m openpifpaf.train \\\n",
    "  --lr=0.0003 --momentum=0.95 --clip-grad-value=10 \\\n",
    "  --epochs=150 \\\n",
    "  --lr-decay 130 140 --lr-decay-epochs=10 \\\n",
    "  --batch-size=64 \\\n",
    "  --weight-decay=1e-5 \\\n",
    "  --dataset=cocodet --cocodet-upsample=2 \\\n",
    "  --basenet=resnet18 --resnet-input-conv2-stride=2 --resnet-block5-dilation=2\n",
    "```\n",
    "\n",
    "with eval code:\n",
    "\n",
    "```sh\n",
    "CUDA_VISIBLE_DEVICES=0 python3 -m openpifpaf.eval \\\n",
    "  --watch --checkpoint \"outputs/resnet??-*-cocodet*.pkl.epoch??[0,5]\" \\\n",
    "  --loader-workers=2 \\\n",
    "  --dataset=cocodet --decoder=cifdet:0 \\\n",
    "  --seed-threshold=0.1 --instance-threshold=0.1\n",
    "```\n",
    "\n",
    "COCO AP=24.3% at epoch 90 (now outdated)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Cloud and HPC\n",
    "\n",
    "OpenPifPaf can be trained on cloud infrastructure and in high performance computing (HPC) environments. We have good experience with the [pytorch/pytorch docker containers](https://hub.docker.com/r/pytorch/pytorch/tags). The `latest` tag does not include any development packages like `gcc`. For that, you need a \"devel\" package (e.g. [pytorch/1.6.0-cuda10.1-cudnn7-devel](https://hub.docker.com/layers/pytorch/pytorch/1.6.0-cuda10.1-cudnn7-devel/images/sha256-ccebb46f954b1d32a4700aaeae0e24bd68653f92c6f276a608bf592b660b63d7?context=explore)).\n",
    "\n",
    "This is a development flow that worked well for us:\n",
    "* start with an empty directory\n",
    "* launch the \"devel\" container and bind/link your current empty directory\n",
    "* create a Python virtualenv and install all software from within the devel container into the virtualenv\n",
    "* never modify this virtualenv from the host, always from within the devel container\n",
    "\n",
    "After setting up your environment, you can execute the training. You can even use the container without the \"devel\" tools to submit the job. \n",
    "\n",
    "__HPC job submission__: Our environment supports [Slurm](https://slurm.schedmd.com/quickstart.html) and [Singularity](https://sylabs.io/guides/3.6/user-guide/). Run \n",
    "\n",
    "```sh\n",
    "sbatch ./train.sh \\\n",
    "  --lr=0.0003  # and all the other parameters for training from above\n",
    "```\n",
    "\n",
    "with `train.sh` containing:\n",
    "\n",
    "```sh\n",
    "#!/bin/bash -l\n",
    "#SBATCH --nodes 1\n",
    "#SBATCH --ntasks 1\n",
    "#SBATCH --cpus-per-task 40\n",
    "#SBATCH --mem 96G\n",
    "#SBATCH --time 72:00:00\n",
    "#SBATCH --account=<your-account-name>\n",
    "#SBATCH --gres gpu:2\n",
    "\n",
    "srun singularity exec --bind <make-host-dir-available> --nv pytorch_latest.sif \\\n",
    "  /bin/bash -c \"source ./venv3/bin/activate && python3 -u -m openpifpaf.train $(printf '%q ' \"$@\")\"\n",
    "```\n",
    "\n",
    "Similarly, an `eval.sh` script might be run with \n",
    "\n",
    "```sh\n",
    "sbatch ./eval.sh \\\n",
    "  --watch --checkpoint \"outputs/shufflenetv2k16-201023-163830-cocokp.pkl.epoch??[0,5]\" \\\n",
    "  --dataset=cocokp \\\n",
    "  --loader-workers=2 \\\n",
    "  --force-complete-pose --seed-threshold=0.2\n",
    "```\n",
    "\n",
    "and look like this:\n",
    "\n",
    "```sh\n",
    "#!/bin/bash -l\n",
    "#SBATCH --nodes 1\n",
    "#SBATCH --ntasks 1\n",
    "#SBATCH --cpus-per-task 20\n",
    "#SBATCH --mem 48G\n",
    "#SBATCH --time 06:00:00\n",
    "#SBATCH --account=<your-account-name>\n",
    "#SBATCH --gres gpu:1\n",
    "\n",
    "pattern=$1\n",
    "shift\n",
    "\n",
    "srun singularity exec --bind <make-host-dir-available> --nv pytorch_latest.sif \\\n",
    "  /bin/bash -c \"source ./venv3/bin/activate && python3 -m openpifpaf.eval $(printf '%q ' \"$@\")\"\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## DistributedDataParallel\n",
    "\n",
    "This mode of parallelization is activated with the `--ddp` argument.\n",
    "With `torch.distributed.launch`:\n",
    "\n",
    "```sh\n",
    "python -m torch.distributed.launch \\\n",
    "  -m openpifpaf.train --ddp \\\n",
    "  --all-other-args\n",
    "```\n",
    "\n",
    "By default, this will replace all BatchNorms in the model with SyncBatchNorms.\n",
    "The only argument that behaves differently is `--batch-size` which is \n",
    "the total batch size across all GPUs with DataParallel but here with \n",
    "DistributedDataParallel it is the batch size of a single process/GPU.\n",
    "This is the behavior of PyTorch in these two modes and we did not add\n",
    "any code to change that.\n",
    "On SLURM, the equivalent sbatch script is:\n",
    "\n",
    "```sh\n",
    "#!/bin/bash -l\n",
    "#SBATCH --ntasks 4\n",
    "#SBATCH --cpus-per-task 20\n",
    "#SBATCH --mem 48G\n",
    "#SBATCH --time 72:00:00\n",
    "#SBATCH --gres gpu:2  # or as many GPUs as you have in a node\n",
    "\n",
    "srun --gpu-bind=closest singularity exec --bind /scratch/izar --nv pytorch_latest.sif \\\n",
    "  /bin/bash -c \"source ./venv3/bin/activate && MASTER_ADDR=$(hostname) MASTER_PORT=7142 python3 -u -m openpifpaf.train --ddp $(printf \"%s \" \"$@\")\"\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3.7.4 64-bit ('venv3': venv)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.2"
  },
  "metadata": {
   "interpreter": {
    "hash": "ea6946363a43e80d241452ab397f4c58bdd3d2517da174158e9c46ce6717422a"
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
